import Logger from "./utils/Logger"
import TypeLookup from "./utils/TypeLookup"
import OpenDeferred from "./utils/OpenDeferred"
import funcUtil from "./utils/func.util"
import EngineContext from "./EngineContext"
import moduleEnum from "./enum/module.enum"
import Module from "./module/Module"
const n = new Logger("module-loader")

export interface itemData<A = any, B = any> {
  es6ModulePromise: (name: string) => Promise<B>
  ctorPromise: OpenDeferred<A>
  initPromise: OpenDeferred<A>
  state: number
  module?: any
}

export default class ModuleLoader {
  registeredModules: Record<string, itemData>
  typeLookup: TypeLookup
  constructor() {
    this.registeredModules = {}
    this.typeLookup = new TypeLookup()
  }
  registerModule(e: typeof Module, i: () => Promise<any>) {
    const n = this.typeLookup.addType(e),
      r = {
        es6ModulePromise: i,
        ctorPromise: new OpenDeferred(),
        initPromise: new OpenDeferred(),
        state: moduleEnum.Registered
      }
    this.registeredModules[n] = r
  }
  getTypeID(e: typeof Module) {
    const t = this.typeLookup.getKeyByType(e)
    if (!t) throw Error("Trying to access unregistered Module")
    return t
  }
  getTypeFromTypeID(e: string) {
    return this.typeLookup.getTypeByKey(e)
  }
  moduleRegistered(e: typeof Module) {
    return !!this.typeLookup.getKeyByType(e)
  }
  moduleLoaded(e: typeof Module) {
    if (!this.moduleRegistered(e)) return !1
    const t = this.typeLookup.getKeyByType(e),
      i = this.registeredModules[t].state
    return ![moduleEnum.Registered, moduleEnum.Unloaded].includes(i)
  }
  getModule<T extends typeof Module>(t: T): Promise<T["prototype"]> {
    const i = this.typeLookup.getKeyByType(t)
    if (!i) throw Error(`Trying to get ${funcUtil.getClassName(t)} before it was registered!`)
    if (this.registeredModules[i].state === moduleEnum.Unloaded) throw Error(`${funcUtil.getClassName(t)} module unloaded`)
    return this.registeredModules[i].initPromise.nativePromise()
  }
  getModuleSync<A extends typeof Module>(t: A): A["prototype"] {
    const i = this.typeLookup.getKeyByType(t)
    if (!i) throw new Error(`Trying to get ${funcUtil.getClassName(t)} before it was registered!`)
    const n = this.registeredModules[i]
    if (n.state !== moduleEnum.Initialized || !n.module) throw Error(`${funcUtil.getClassName(t)} module not loaded!`)
    return n.module
  }
  loadModule<A extends typeof Module>(t: EngineContext, i: A, r: any = {}) {
    const o = this.typeLookup.getKeyByType(i)
    if (!o) throw Error(`Attempting to load an unregistered module ${funcUtil.getClassName(i)}`)
    const a: itemData<A["prototype"], A> = this.registeredModules[o]
    if (a.state === moduleEnum.Initialized) return a
    if (a.state !== moduleEnum.Registered && a.state !== moduleEnum.Unloaded) throw Error(`${funcUtil.getClassName(i)} already loading`)
    a.state = moduleEnum.Loading
    const l = funcUtil.getClassName(i)
    a.es6ModulePromise(l)
      .then(o => {
        if (!o) throw new Error(`${funcUtil.getClassName(i)} has no default export during module loading`)
        const l = new o()
        a.ctorPromise.resolve(l).then(() => {
          const e = Date.now()
          let i = !1
          setTimeout(() => {
            i || n.debug(`${l.name} init took over 2 seconds`)
          }, 5000)
          l.init(r, t)
            .then(() => {
              n.debug(`${l.name} init took ${(Date.now() - e) / 1000} secs.`)
              i = !0
              a.state = moduleEnum.Initialized
              a.module = l
              a.initPromise.resolve(l)
            })
            .catch(e => {
              a.initPromise.reject(e)
            })
        })
      })
      .catch(t => {
        n.error(`Failed to load es6Module for ${funcUtil.getClassName(i)}: ${t}`)
        a.initPromise.reject(t)
      })
    return a
  }
  whenQueueEmpties() {
    const e = []
    for (const t in this.registeredModules) {
      const i = this.registeredModules[t]
      i.state === moduleEnum.Loading && e.push(i.initPromise)
    }
    return OpenDeferred.all(e).nativePromise()
  }
  unloadModule(e: typeof Module) {
    const i = this.typeLookup.getKeyByType(e)
    if (!i) throw Error("Attempting to unload unregistered module")
    const n = this.registeredModules[i]
    if (n.state === moduleEnum.Unloaded || n.state === moduleEnum.Registered) throw Error("Attempting to unload not-loaded module")
    n.state = moduleEnum.Unloaded
    n.ctorPromise = new OpenDeferred()
    n.initPromise = new OpenDeferred()
    n.module = void 0
  }
}
